Skip to content

fix: studio Vercel API routes returning HTML instead of JSON + CORS for temp domains#1052

Merged
hotlong merged 12 commits intomainfrom
copilot/fix-api-routes-returning-html
Apr 2, 2026
Merged

fix: studio Vercel API routes returning HTML instead of JSON + CORS for temp domains#1052
hotlong merged 12 commits intomainfrom
copilot/fix-api-routes-returning-html

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

All /api/* routes on the Vercel-deployed studio return the SPA's index.html instead of JSON because Vercel doesn't detect the serverless function. Additionally, when deployed with a temporary/preview Vercel domain, cross-origin API requests fail with CORS errors because VITE_SERVER_URL was hardcoded to the production domain, forcing all deployments to call the production API cross-origin.

Root cause

Six issues prevented the serverless function from working:

  1. No committed API route file — Vercel needs a file in the api/ directory to exist pre-build for function detection. The previous api/index.js was only generated during the build step by bundle-api.mjs, so Vercel never recognised it as a serverless function.

  2. CJS/ESM format mismatch — The esbuild bundle used format: 'cjs' (CommonJS), but the studio's package.json has "type": "module". Node.js treats .js files as ESM in type: module projects, so the CJS output (require()/module.exports) would fail to parse at runtime.

  3. Vercel TypeScript compilation conflicts — Committing a .ts stub in api/ caused a chicken-and-egg problem: if the .ts exists, Vercel's @vercel/node builder compiles it and overwrites the self-contained esbuild bundle with a thin re-export referencing ../server/index (which doesn't exist at runtime → ERR_MODULE_NOT_FOUND). Deleting the .ts during build causes Vercel to error with "File not found" since it expects pre-build-detected files to still exist post-build.

  4. CJS→ESM dynamic require of Node.js built-ins — When esbuild converts CJS dependencies (knex/tarn) to ESM format, their require('events') calls are replaced by esbuild's __require shim, which throws "Dynamic require of 'events' is not supported" for Node.js built-in modules.

  5. Missing native addons at runtime — Native C++ modules (better-sqlite3, @libsql/client) are marked as external in esbuild (they can't be bundled), but without functions.includeFiles in vercel.json, Vercel doesn't package them into the deployed serverless function → Cannot find module 'better-sqlite3'.

  6. pnpm strict mode hides native modules from includeFiles — Even with includeFiles configured, pnpm's strict isolation (no shamefully-hoist) stores native modules in the monorepo root node_modules/.pnpm/ store, not in the app-level node_modules/. Since Vercel's includeFiles resolves relative to the function's directory (apps/studio/), the glob node_modules/better-sqlite3/** matches nothing — the modules physically live at ../../node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3/.

Solution — committed .js wrapper + separate esbuild bundle with createRequire + includeFiles + build-time module copy

apps/studio/api/[[...route]].js (new)

  • Committed .js catch-all API route (not .ts) so Vercel detects it pre-build without triggering TypeScript compilation
  • Thin re-export from ./_handler.js — the self-contained esbuild bundle generated during build
  • Using a .js wrapper avoids all three failure modes: the file exists for pre-build detection, is never deleted during build, and Vercel does not TS-compile it

apps/studio/scripts/bundle-api.mjs

  • Changed format from 'cjs' to 'esm' — fixes the "type": "module" compatibility issue
  • Changed outfile from 'api/index.js' to 'api/_handler.js' — outputs to a separate file (prefixed with _ so Vercel treats it as a helper file, not a separate function) that the committed wrapper re-exports
  • Added createRequire banner — provides a real require function in the ESM scope so that esbuild's __require shim (which checks typeof require !== "undefined") uses it instead of throwing for Node.js built-in modules like events, stream, path, etc. required by CJS dependencies (knex/tarn)

apps/studio/scripts/build-vercel.sh

  • Runs esbuild to generate api/_handler.js (the self-contained bundle)
  • Copies external native modules (better-sqlite3, @libsql/client) from the monorepo root node_modules/ into the studio's local node_modules/ — this ensures Vercel's includeFiles glob can find them, since pnpm's strict mode doesn't symlink transitive native dependencies into app-level node_modules/
  • No file deletion or workarounds needed — the committed .js wrapper and the generated _handler.js coexist cleanly

apps/studio/vercel.json

  • Changed VITE_SERVER_URL from "https://play.objectstack.ai" to "" (empty string / same-origin) so each deployment — including previews — calls its own serverless function instead of the production API cross-origin
  • Updated rewrites from { "source": "/api/(.*)", "destination": "/api" } to { "source": "/api/:path*", "destination": "/api/[[...route]]" } matching the hotcrm pattern
  • No outputDirectory — Vercel's default for framework: null already serves static from public/ and functions from api/ at project root
  • Added functions block with includeFiles for native addons (better-sqlite3, @libsql/client) that esbuild marks as external and cannot bundle — these must be explicitly included in the Vercel function's deployment package, matching the same pattern used in hotcrm's vercel.json
{
  "build": {
    "env": {
      "VITE_RUNTIME_MODE": "server",
      "VITE_SERVER_URL": ""
    }
  },
  "functions": {
    "api/**/*.js": {
      "memory": 1024,
      "maxDuration": 60,
      "includeFiles": "{node_modules/@libsql,node_modules/better-sqlite3}/**"
    }
  },
  "rewrites": [
    { "source": "/api/:path*", "destination": "/api/[[...route]]" },
    { "source": "/((?!api/).*)", "destination": "/index.html" }
  ]
}

apps/studio/.gitignore

  • Changed from ignoring the entire api directory to only ignoring build artifacts (api/_handler.js, api/_handler.js.map), so the committed api/[[...route]].js wrapper is tracked

apps/studio/server/index.ts

  • Extracted getVercelOrigins() helper that collects all Vercel deployment URLs from env vars (VERCEL_URL, VERCEL_BRANCH_URL, VERCEL_PROJECT_PRODUCTION_URL). Reused for both the CORS middleware and better-auth trustedOrigins to prevent allowlist drift.
  • Added hono/cors middleware to the outer Hono app as a safety net for any remaining cross-origin scenarios (e.g., custom domain configurations)
  • The middleware is placed before the catch-all route so OPTIONS preflight requests are answered immediately without waiting for kernel cold-start
  • Dynamically allowed origins:
    • All Vercel deployment URLs via getVercelOrigins()
    • Any *.vercel.app subdomain (covers all preview/branch deployments)
    • localhost for local development
  • Supports credentials: true for authenticated cross-origin requests

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Apr 2, 2026 8:11am
spec Ready Ready Preview, Comment Apr 2, 2026 8:11am

Request Review

…tead of HTML

- Add outputDirectory: "public" to vercel.json
- Add functions block declaring api/index.js as serverless function
- Fix API rewrite destination from /api to /api/index.js
- Update CHANGELOG.md

Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/d0116abe-0010-46e2-a25d-08bc5236156b

Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix API routes returning HTML instead of JSON on Vercel fix: studio Vercel API routes returning HTML instead of JSON Apr 2, 2026
Copilot AI requested a review from xuyushun441-sys April 2, 2026 03:43
@xuyushun441-sys
Copy link
Copy Markdown
Contributor

@copilot vercel 部署使用临时域名时,报跨域错误

Add hono/cors middleware to the outer Hono app in server/index.ts so the
serverless function returns proper Access-Control-Allow-Origin headers for
cross-origin requests from Vercel preview/temporary domains.

Allowed origins:
- All Vercel deployment URLs from VERCEL_URL, VERCEL_BRANCH_URL,
  VERCEL_PROJECT_PRODUCTION_URL env vars
- Any *.vercel.app subdomain (covers all preview deployments)
- localhost for local development

The CORS middleware is placed before the catch-all route so OPTIONS
preflight requests are handled immediately without kernel boot latency.

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/324fd769-65e6-430a-aab8-06ad343e75f2

Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot vercel 部署使用临时域名时,报跨域错误

Added Hono CORS middleware to the outer app in server/index.ts (commit e95f3a9). The serverless function now returns proper Access-Control-Allow-Origin headers for cross-origin requests.

Allowed origins:

  • All Vercel deployment URLs from env vars (VERCEL_URL, VERCEL_BRANCH_URL, VERCEL_PROJECT_PRODUCTION_URL)
  • Any *.vercel.app subdomain (covers all preview/temporary deployments)
  • localhost for local development

The CORS middleware is placed on the outer Hono app before the catch-all route, so OPTIONS preflight requests are answered immediately without waiting for kernel cold-start.

Copilot AI changed the title fix: studio Vercel API routes returning HTML instead of JSON fix: studio Vercel API routes returning HTML instead of JSON + CORS for temp domains Apr 2, 2026
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 2, 2026

@copilot
The pattern "api/index.js" defined in functions doesn't match any Serverless Functions inside the api directory.

…ion error

The `functions` block pattern `api/index.js` fails Vercel's pre-build
validation because the file is generated during the build step (by
bundle-api.mjs), not available beforehand.

The in-code `export const config` in server/index.ts already configures
memory and maxDuration — Vercel's @vercel/node runtime picks it up from
the deployed function bundle without any vercel.json pattern matching.

Also reverts the rewrite destination to `/api` (the auto-detected route
for api/index.js) instead of `/api/index.js` (a file path that doesn't
map to a Vercel route).

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/16819ebf-4d21-4ae9-892d-8c1593b21504

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot
The pattern "api/index.js" defined in functions doesn't match any Serverless Functions inside the api directory.

Removed the functions block from vercel.json in 399f602. The api/index.js file is generated during the build step by bundle-api.mjs, so it doesn't exist when Vercel validates the functions patterns pre-build.

The function config (memory/maxDuration) is already handled by the in-code export const config in server/index.ts (line 293), which Vercel's @vercel/node runtime picks up from the deployed bundle without needing a vercel.json pattern match.

Also reverted the rewrite destination to /api (the route path auto-detected from api/index.js) instead of /api/index.js (which is a file path, not a route).

Copilot AI requested a review from hotlong April 2, 2026 05:19
@hotlong hotlong marked this pull request as ready for review April 2, 2026 05:36
Copilot AI review requested due to automatic review settings April 2, 2026 05:36
… Vercel TS compilation and file-not-found errors

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/656a5b4f-383c-4318-a657-a952f4445583

Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot File not found: /vercel/path0/apps/studio/api/[[...route]].ts

Fixed in cfc2227. Replaced the .ts stub with a committed .js wrapper (api/[[...route]].js) that re-exports from a separately-named esbuild bundle (api/_handler.js).

This avoids the chicken-and-egg problem:

  • .ts stub → Vercel TS-compiles it, overwrites the esbuild bundle → ERR_MODULE_NOT_FOUND
  • Deleting .ts during build → File not found
  • .js wrapper + separate _handler.js → Vercel uses the .js directly (no TS compilation), the file is never deleted, and it loads the self-contained bundle at runtime

@xuyushun441-sys
Copy link
Copy Markdown
Contributor

还是不对,能不能认真参考 https://github.com/objectstack-ai/hotcrm/blob/main/vercel.json

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/var/task/apps/studio/server/index' imported from /var/task/apps/studio/api/[[...route]].js
at finalizeResolution (node:internal/modules/esm/resolve:280:11)
at moduleResolve (node:internal/modules/esm/resolve:870:10)
at moduleResolveWithNodePath (node:internal/modules/esm/resolve:996:14)
at defaultResolve (node:internal/modules/esm/resolve:1039:79)
at #cachedDefaultResolve (node:internal/modules/esm/loader:718:20)
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:735:38)
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:764:52)
at #resolve (node:internal/modules/esm/loader:700:17)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:620:35)
at ModuleJob.syncLink (node:internal/modules/esm/module_job:143:33) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///var/task/apps/studio/server/index'
}
Node.js process exited with exit status: 1. The logs above can help with debugging the issue.

@xuyushun441-sys
Copy link
Copy Markdown
Contributor

@copilot Error: Dynamic require of "events" is not supported
at file:///var/task/apps/studio/api/_handler.js:16:9
at ../../node_modules/.pnpm/tarn@3.0.2/node_modules/tarn/dist/Pool.js (file:///var/task/apps/studio/api/_handler.js:18787:20)
at __require2 (file:///var/task/apps/studio/api/_handler.js:22:50)
at ../../node_modules/.pnpm/tarn@3.0.2/node_modules/tarn/dist/tarn.js (file:///var/task/apps/studio/api/_handler.js:19209:18)
at __require2 (file:///var/task/apps/studio/api/_handler.js:22:50)
at ../../node_modules/.pnpm/knex@3.2.7_better-sqlite3@12.8.0_mysql2@3.15.3/node_modules/knex/lib/client.js (file:///var/task/apps/studio/api/_handler.js:32930:34)
at __require2 (file:///var/task/apps/studio/api/_handler.js:22:50)
at ../../node_modules/.pnpm/knex@3.2.7_better-sqlite3@12.8.0_mysql2@3.15.3/node_modules/knex/lib/knex-builder/Knex.js (file:///var/task/apps/studio/api/_handler.js:42924:18)
at __require2 (file:///var/task/apps/studio/api/_handler.js:22:50)
at ../../node_modules/.pnpm/knex@3.2.7_better-sqlite3@12.8.0_mysql2@3.15.3/node_modules/knex/lib/index.js (file:///var/task/apps/studio/api/_handler.js:42977:16)
Node.js process exited with exit status: 1. The logs above can help with debugging the issue.

…-in require() support

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/3888d7e6-570c-478b-b969-84043c288a94

Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot Error: Dynamic require of "events" is not supported
at file:///var/task/apps/studio/api/_handler.js:16:9
at ../../node_modules/.pnpm/tarn@3.0.2/node_modules/tarn/dist/Pool.js (file:...

Fixed in 6886a92. Added createRequire banner to the esbuild config:

import { createRequire } from "module";
const require = createRequire(import.meta.url);

When esbuild converts CJS dependencies (knex/tarn) to ESM format, their require('events') calls go through esbuild's __require shim which throws "Dynamic require is not supported". The shim checks typeof require !== "undefined" and uses the real require when available — the createRequire banner provides that real require in the ESM scope.

还是不对,能不能认真参考 https://github.com/objectstack-ai/hotcrm/blob/main/vercel.json

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/var/task/apps/studio/server/index' imported fr...

The ERR_MODULE_NOT_FOUND for ../server/index was from the previous approach (.ts stub). The cfc2227 commit fixed that by switching to a committed .js wrapper + separate _handler.js bundle.

The follow-up "Dynamic require of events" error (from _handler.js) is fixed in 6886a92 with a createRequire banner in the esbuild config.

The studio uses esbuild bundling (instead of hotcrm's native Vercel TS compilation) because workspace packages at ../../packages/ can't be reliably resolved by Vercel's @vercel/node builder in a pnpm monorepo. The esbuild bundle is self-contained.

@xuyushun441-sys
Copy link
Copy Markdown
Contributor

@copilot 2026-04-02 07:46:05.148 [info] [Vercel] GET http://objectstack-play-git-copilot-fix-api-routes-d603fc-object-stack.vercel.app/api/v1/meta?path=v1%2Fmeta
2026-04-02 07:46:05.148 [info] [Vercel] Booting ObjectStack Kernel (server mode)...
2026-04-02 07:46:05.151 [info] {"timestamp":"2026-04-02T07:46:05.146Z","level":"info","message":"Loading plugin: com.objectstack.engine.objectql"}
2026-04-02 07:46:05.156 [info] {"timestamp":"2026-04-02T07:46:05.151Z","level":"info","message":"Plugin loaded: com.objectstack.engine.objectql (5ms)"}
2026-04-02 07:46:05.157 [info] {"timestamp":"2026-04-02T07:46:05.156Z","level":"info","message":"Plugin registered: com.objectstack.engine.objectql@1.0.0","plugin":"com.objectstack.engine.objectql","version":"1.0.0"}
2026-04-02 07:46:05.161 [info] Knex: run
$ npm install better-sqlite3 --save
Cannot find module 'better-sqlite3'
Require stack:

  • /var/task/apps/studio/api/_handler.js
    Error: Cannot find module 'better-sqlite3'
    Require stack:
  • /var/task/apps/studio/api/_handler.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1421:15)
    at defaultResolveImpl (node:internal/modules/cjs/loader:1059:19)
    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1064:22)
    at Module. (node:internal/modules/cjs/loader:1227:37)
    at /opt/rust/nodejs.js:2:13531
    at Module.pn (/opt/rust/nodejs.js:2:13909)
    at Xe.e..Ye._load (/opt/rust/nodejs.js:2:13501)
    at TracingChannel.traceSync (node:diagnostics_channel:328:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:245:24)
    at Module.require (node:internal/modules/cjs/loader:1504:12)
    2026-04-02 07:46:05.161 [error] [Vercel] Kernel boot failed: Knex: run
    $ npm install better-sqlite3 --save
    Cannot find module 'better-sqlite3'
    Require stack:
  • /var/task/apps/studio/api/_handler.js
    2026-04-02 07:46:05.162 [error] [Vercel] Handler error: Knex: run
    $ npm install better-sqlite3 --save
    Cannot find module 'better-sqlite3'
    Require stack:
  • /var/task/apps/studio/api/_handler.js
    2026-04-02 07:46:05.165 [warning] [Logger] Pino not available, falling back to console: Error: Cannot find module 'pino'
    Require stack:
  • /var/task/apps/studio/api/_handler.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1421:15)
    at defaultResolveImpl (node:internal/modules/cjs/loader:1059:19)
    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1064:22)
    at Module. (node:internal/modules/cjs/loader:1227:37)
    at /opt/rust/nodejs.js:2:13531
    at Module.pn (/opt/rust/nodejs.js:2:13909)
    at Xe.e..Ye._load (/opt/rust/nodejs.js:2:13501)
    at TracingChannel.traceSync (node:diagnostics_channel:328:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:245:24)
    at Module.require (node:internal/modules/cjs/loader:1504:12) {
    code: 'MODULE_NOT_FOUND',
    requireStack: [ '/var/task/apps/studio/api/_handler.js' ]
    }

…ibsql/client) on Vercel

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/fbeefcaa-1bf8-418e-aed0-baafcf1b18aa

Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot 2026-04-02 07:46:05.148 [info] [Vercel] GET http://objectstack-play-git-copilot-fix-api-routes-d603fc-object-stack.vercel.app/api/v1/meta?path=v1%2Fmeta
2026-04-02 07:46:05.148 [info] [Verce...

Fixed in 1bc22cd. Added functions.includeFiles in vercel.json to include better-sqlite3 and @libsql/client — these are native addons marked as external in esbuild (can't be bundled), so they must be explicitly included in the Vercel function's deployment package. Matches the same pattern used in hotcrm's vercel.json.

@xuyushun441-sys
Copy link
Copy Markdown
Contributor

@copilot [Vercel] Kernel boot failed: Knex: run
$ npm install better-sqlite3 --save
Cannot find module 'better-sqlite3'
Require stack:

  • /var/task/apps/studio/api/_handler.js
    [Vercel] Handler error: Knex: run
    $ npm install better-sqlite3 --save
    Cannot find module 'better-sqlite3'
    Require stack:
  • /var/task/apps/studio/api/_handler.js
    [Logger] Pino not available, falling back to console: Error: Cannot find module 'pino'
    Require stack:
  • /var/task/apps/studio/api/_handler.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1421:15)
    at defaultResolveImpl (node:internal/modules/cjs/loader:1059:19)
    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1064:22)
    at Module. (node:internal/modules/cjs/loader:1227:37)
    at /opt/rust/nodejs.js:2:13531
    at Module.pn (/opt/rust/nodejs.js:2:13909)
    at Xe.e..Ye._load (/opt/rust/nodejs.js:2:13501)
    at TracingChannel.traceSync (node:diagnostics_channel:328:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:245:24)
    at Module.require (node:internal/modules/cjs/loader:1504:12) {
    code: 'MODULE_NOT_FOUND',
    requireStack: [ '/var/task/apps/studio/api/_handler.js' ]
    }

…ict mode on Vercel

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/5da7c570-c9a4-4a8d-93d4-bcb11dad0da9

Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot [Vercel] Kernel boot failed: Knex: run
$ npm install better-sqlite3 --save
Cannot find module 'better-sqlite3'
...

Fixed in 0fa5419. The includeFiles pattern was matching nothing because apps/studio/node_modules/better-sqlite3 doesn't exist — this monorepo uses pnpm's strict mode (no shamefully-hoist like hotcrm), so transitive native dependencies aren't symlinked into app-level node_modules/.

Added a build step in build-vercel.sh that copies native external modules (better-sqlite3, @libsql/*) from the monorepo root's ../../node_modules/ into the studio's local node_modules/ using cp -rL (follows pnpm symlinks). The includeFiles pattern now finds the actual files for Vercel's function packaging.

@hotlong hotlong merged commit 43ee0d0 into main Apr 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] studio 部署到 Vercel 后,API 路由返回 HTML 而不是 JSON

4 participants